//@author A0081058H
package net.mysoc.w111j.ui.generatereport;

import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.PieChart;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.HBoxBuilder;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import net.mysoc.w111j.model.Expense;
import net.mysoc.w111j.model.Money;
import net.mysoc.w111j.service.Report;
import net.mysoc.w111j.service.ReportService;
import net.mysoc.w111j.service.ReportType;
import net.mysoc.w111j.ui.Constants;
import net.mysoc.w111j.util.Layout;
import net.mysoc.w111j.util.Stats;

/**
 * The reports view.
 */
public class GenerateReportView extends BorderPane {
    private GenerateReportPresenter presenter;
    private final GenerateReportModel model;
    private SingleSelectionModel<Tab> selectedTab;

    // Tabs are stored so we can rebuild them individually on demand
    private TabPane tabpane;
    private Tab dayTab;
    private Tab weekTab;
    private Tab monthTab;
    private Tab yearTab;

    /**
     * Constructor. Links the {@code GenerateReportModel} linked to this view, and builds an incomplete view. See
     * buildView() and buildCharts().
     *
     * @param model the {@code GenerateReportModel} linked to this view.
     */
    public GenerateReportView(GenerateReportModel model) {
        this.model = model;

        buildView();
    }

    /**
     * Builds the view. Only builds the window controls as data cannot be retrieved before the presenter is set.
     * buildCharts() will retrieve relevant data -- call it to set up charts.
     */
    private void buildView() {
        HBox windowControl = HBoxBuilder.create().children(addOKButton()).styleClass("window-control").build();

        setBottom(windowControl);
    }

    /**
     * Sets the {@code GenerateReportPresenter} linked to this view.
     *
     * @param presenter the {@code GenerateReportPresenter} linked to this view.
     */
    public void setPresenter(GenerateReportPresenter presenter) {
        this.presenter = presenter;
    }

    /**
     * Builds new charts.
     */
    public void buildCharts() {
        setTop(addReportsTitle());
        setCenter(addTabPane());
    }

    /**
     * Refreshes the currently selected tab.
     */
    public void refreshSelectedTab() {
        String selectedTabName = selectedTab.selectedItemProperty().get().getText();
        switch (selectedTabName) {
        case "DAY":
            dayTab.setContent(addDayTab());
            break;
        case "WEEK":
            weekTab.setContent(addWeekTab());
            break;
        case "MONTH":
            monthTab.setContent(addMonthTab());
            break;
        case "YEAR":
            yearTab.setContent(addYearTab());
            break;
        }
    }

    /**
     * Creates the OK {@code Button} for this dialog. This calls the presenter to read input from the
     * {@code ExpenseDetailModel}
     *
     * @return an OK {@code Button}
     */
    private Button addOKButton() {
        Button button = new Button("OK");
        button.setDefaultButton(true); // Handles VK_ENTER
        button.setCancelButton(true); // Handles VK_ESC as well, since we have
                                      // no cancel button

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                // Closes the dialog window from inside it
                // TODO: handle case when there this is not in a dialog window
                Node source = (Node) event.getSource();
                Stage stage = (Stage) source.getScene().getWindow();
                stage.close();
                presenter.handleOK();
            }
        });

        button.getStyleClass().addAll("button-ok", "generate-report-view-buttonok");

        return button;
    }

    /**
     * Creates and returns the reports header.
     *
     * @return a {@code HBox} containing the reports header.
     */
    private HBox addReportsTitle() {
        HBox title = new HBox();
        Text text = new Text("Reports");

        title.getChildren().addAll(text);

        text.getStyleClass().addAll("typo", "page-header");
        title.getStyleClass().addAll("generate-report-view-text");

        return title;
    }

    /**
     * Creates, links, and returns the {@code TabPane} containing individual {@code Tab}s for each report type (daily,
     * weekly...)
     *
     * @return the {@code TabPane} containing charts.
     */
    private HBox addTabPane() {
        HBox wrapper = new HBox();

        if (tabpane == null) {
            tabpane = new TabPane();
            dayTab = new Tab("DAY");
            weekTab = new Tab("WEEK");
            monthTab = new Tab("MONTH");
            yearTab = new Tab("YEAR");
            tabpane.getTabs().addAll(dayTab, weekTab, monthTab, yearTab);
            tabpane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
            selectedTab = tabpane.getSelectionModel();
            // Default to monthly
            selectedTab.select(2);
            model.setStartDate(ReportService.getStartDateForCurrentPeriod(ReportType.MONTHLY));
        }

        if (dayTab.getContent() == null) {
            dayTab.setContent(addDayTab());
        }

        if (weekTab.getContent() == null) {
            weekTab.setContent(addWeekTab());
        }

        if (monthTab.getContent() == null) {
            monthTab.setContent(addMonthTab());
        }

        if (yearTab.getContent() == null) {
            yearTab.setContent(addYearTab());
        }

        // Sets starting date based off tab, rebuild tab on demand
        selectedTab.selectedItemProperty().addListener(new ChangeListener<Tab>() {
            @Override
            public void changed(ObservableValue<? extends Tab> tab, Tab oldTab, Tab newTab) {
                switch (newTab.getText()) {
                case "DAY":
                    model.setStartDate(ReportService.getToday());
                    break;
                case "WEEK":
                    model.setStartDate(ReportService.getStartDateForCurrentPeriod(ReportType.WEEKLY));
                    break;
                case "MONTH":
                    model.setStartDate(ReportService.getStartDateForCurrentPeriod(ReportType.MONTHLY));
                    break;
                case "YEAR":
                    model.setStartDate(ReportService.getStartDateForCurrentPeriod(ReportType.YEARLY));
                    break;
                }
            }
        });

        wrapper.getChildren().addAll(tabpane);
        wrapper.getStyleClass().addAll("generate-report-view-tabpane-wrapper");

        return wrapper;
    }

    /**
     * Creates and formats the "Day" tab. This separation is required as not all charts are meaningful for different
     * report types.
     *
     * @return the day {@code Tab}.
     */
    private HBox addDayTab() {
        HBox hbox = new HBox();
        ReportType reportType = ReportType.DAILY;
        Report report = this.presenter.handleGetData(reportType);

        if (report.getExpenses().size() == 0) {
            hbox.getChildren().addAll(addNoExpenses(reportType));
        } else {
            VBox stats = addStats(report);
            PieChart pieChart = addPieChart(report);
            ScatterChart<Number, Number> scatterChart = addScatterChart(report);
            hbox.getChildren().addAll(wrapScrollPane(stats, scatterChart, pieChart));
        }

        hbox.setPrefWidth(Integer.MAX_VALUE);

        return hbox;
    }

    /**
     * Creates and formats the "Week" tab.
     *
     * @return the week {@code Tab}.
     */
    private HBox addWeekTab() {
        HBox hbox = new HBox();
        ReportType reportType = ReportType.WEEKLY;
        Report report = this.presenter.handleGetData(reportType);

        if (report.getExpenses().size() == 0) {
            hbox.getChildren().addAll(addNoExpenses(reportType));
        } else {
            VBox stats = addStats(report);
            PieChart pieChart = addPieChart(report);
            ScatterChart<Number, Number> scatterChart = addScatterChart(report);
            hbox.getChildren().addAll(wrapScrollPane(stats, scatterChart, pieChart));
        }

        hbox.setPrefWidth(Integer.MAX_VALUE);

        return hbox;
    }

    /**
     * Creates and formats the "Month" tab.
     *
     * @return the month {@code Tab}.
     */
    private HBox addMonthTab() {
        HBox hbox = new HBox();
        ReportType reportType = ReportType.MONTHLY;
        Report report = this.presenter.handleGetData(reportType);

        if (report.getExpenses().size() == 0) {
            hbox.getChildren().addAll(addNoExpenses(reportType));
        } else {
            VBox stats = addStats(report);
            PieChart pieChart = addPieChart(report);
            ScatterChart<Number, Number> scatterChart = addScatterChart(report);
            hbox.getChildren().addAll(wrapScrollPane(stats, scatterChart, pieChart));
        }

        hbox.setPrefWidth(Integer.MAX_VALUE);

        return hbox;
    }

    /**
     * Creates and formats the "Year" tab.
     *
     * @return the year {@code Tab}.
     */
    private HBox addYearTab() {
        HBox hbox = new HBox();
        ReportType reportType = ReportType.YEARLY;
        Report report = this.presenter.handleGetData(reportType);

        if (report.getExpenses().size() == 0) {
            hbox.getChildren().addAll(addNoExpenses(reportType));
        } else {
            VBox stats = addStats(report);
            PieChart pieChart = addPieChart(report);
            ScatterChart<Number, Number> scatterChart = addScatterChart(report);
            hbox.getChildren().addAll(wrapScrollPane(stats, scatterChart, pieChart));
        }

        hbox.setPrefWidth(Integer.MAX_VALUE);

        return hbox;
    }

    /**
     * Wraps multiple {@code nodes} in a {@code HBox}, and then in a {@code ScrollPane}.
     * This is for scrollbars -- if we have too many charts they will overflow.
     *
     * @param nodes {@code Nodes} to wrap in this {@code ScrollPane}.
     * @return a {@code ScrollPane} wrapping the nodes passed in.
     */
    private ScrollPane wrapScrollPane(Node... nodes) {
        ScrollPane scrollPane = new ScrollPane();
        HBox hbox = new HBox();
        hbox.getChildren().addAll(nodes);

        scrollPane.setPrefWidth(Integer.MAX_VALUE);
        scrollPane.setFitToHeight(true);
        scrollPane.setFitToWidth(true);
        scrollPane.setContent(hbox);

        HBox.setHgrow(scrollPane, Priority.ALWAYS);
        hbox.getStyleClass().addAll("generate-report-view-tab-content");
        scrollPane.getStyleClass().addAll("generate-report-view-scrollpanewrapper");

        return scrollPane;
    }

    /**
     * Creates and adds the date {@code TextField} for entering the start date.
     *
     * @return the date picker wrapped in a {@code VBox} for styling purposes.
     */
    private VBox addDatePicker(final ReportType type) {
        VBox vbox = new VBox();
        Text dateHeader = new Text("Start date");
        TextField startDate = new TextField();
        startDate.setPromptText("4 Apr 1984");
        startDate.setMinWidth(140);
        startDate.setMaxWidth(140);
        startDate.textProperty().bindBidirectional(this.model.startDateStringProperty());

        // Sets date property from textfield
        startDate.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                SimpleDateFormat sdf = Constants.DATE_FORMAT;
                Date date;
                try {
                    date = sdf.parse(model.startDateStringProperty().get());
                    model.startDateProperty().set(date);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        });

        // Offset controls
        Button minusOffsetButton = new Button("", new ImageView("arrowleftw12.png"));
        minusOffsetButton.setTooltip(new Tooltip("To the past"));

        minusOffsetButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                presenter.handleOffset(-1, type);
            }
        });

        Button addOffsetButton = new Button("", new ImageView("arrowrightw12.png"));
        addOffsetButton.setTooltip(new Tooltip("Back to the future"));

        addOffsetButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                presenter.handleOffset(1, type);
            }
        });

        HBox offsetControls = new HBox();
        offsetControls.getStyleClass().addAll("generate-report-view-date-pickeroffset");
        offsetControls.getChildren().addAll(minusOffsetButton, Layout.hboxFloatRight(), addOffsetButton);
        // End offset controls

        dateHeader.getStyleClass().addAll("typo", "page-subheader");
        startDate.getStyleClass().addAll("generate-report-view-date-pickertextfield");
        vbox.getStyleClass().addAll("generate-report-view-date-pickerbox");

        vbox.getChildren().addAll(dateHeader, startDate, offsetControls);
        return vbox;
    }

    /**
     * Returns the content for no expenses.
     *
     * @return the content for no expenses.
     */
    private VBox addNoExpenses(ReportType type) {
        VBox vbox = new VBox();
        vbox.getChildren().add(addDatePicker(type));

        Text noExpenses = new Text("No expenses.");
        noExpenses.getStyleClass().addAll("typo", "page-subheader");
        vbox.getChildren().addAll(noExpenses);
        return vbox;
    }

    /**
     * Creates and returns the statistics box for a given {@code ReportType}
     *
     * @param type the type of statistics to generate for.
     * @return the statistics box for a given {@code ReportType}.
     */
    private VBox addStats(Report report) {
        VBox vbox = new VBox();
        vbox.getChildren().add(addDatePicker(report.getType()));

        Map<String, Money> stats = Stats.getStats(report.getExpenses());

        Text header = new Text("Statistics");
        Text statsText = new Text(report.getExpenses().size()
                + (report.getExpenses().size() == 1 ? " expense" : " expenses") + "\n$" + stats.get("sum") + " sum\n$"
                + stats.get("mean") + " mean\n$" + stats.get("standardDeviation") + " std. dev.");

        header.getStyleClass().addAll("typo", "page-subheader");
        statsText.getStyleClass().addAll("typo", "item-subheader");
        vbox.getChildren().addAll(header, statsText);

        vbox.getStyleClass().addAll("generate-report-view-statistics-box");
        return vbox;
    }

    /**
     * Creates a {@code ScatterChart} for the given {@code ReportType}.
     *
     * This chart gives a quick glance at spending habits. Axes of the chart are X: day of week, Y: expense price.
     * Categories each have a different symbol. Trends are easily spotted (e.g., a user might spend a lot on food on
     * Sundays and very little on bills.)
     *
     * This chart is a number line for daily reports.
     *
     * @param reportType the date range to get data for the chart.
     * @return a {@code ScatterChart} for the given {@code ReportType}.
     */
    private ScatterChart<Number, Number> addScatterChart(Report report) {
        List<XYChart.Series<Number, Number>> dataBySeries = report.getScatterChartData();
        NumberAxis xAxis = null;

        if (report.getExpenses().size() == 0) {
            xAxis = new NumberAxis();
        } else if (report.getType() == ReportType.DAILY) {
            XYChart.Series<Number, Number> series = dataBySeries.get(0);
            int day = (Integer) series.getData().get(0).getXValue();
            xAxis = new NumberAxis(day - 0.5, day + 0.5, 0.5);
            xAxis.setTickLabelsVisible(false);
            xAxis.setTickMarkVisible(false);
            xAxis.setMinorTickVisible(false);
            xAxis.setLabel("");
        } else {
            xAxis = new NumberAxis(1, 7, 1);
            xAxis.setLabel("Day of week");
            // Use abbreviated day names for axis values
            xAxis.setMinorTickVisible(false);
            xAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(xAxis) {
                String[] shortWeekdays = new DateFormatSymbols().getShortWeekdays();

                @Override
                public String toString(Number day) {
                    return shortWeekdays[day.intValue()];
                }
            });
        }

        NumberAxis yAxis = new NumberAxis();
        yAxis.setLabel("Price");

        ScatterChart<Number, Number> scatterChart = new ScatterChart<>(xAxis, yAxis);

        // Stretch
        if (report.getType() == ReportType.DAILY) {
            scatterChart.setMaxWidth(40);
        }
        VBox.setVgrow(scatterChart, Priority.ALWAYS);
        HBox.setHgrow(scatterChart, Priority.ALWAYS);

        scatterChart.getData().addAll(dataBySeries);

        for (XYChart.Series<Number, Number> s : scatterChart.getData()) {
            for (XYChart.Data<Number, Number> d : s.getData()) {
                Expense e = (Expense) d.getExtraValue();
                Tooltip.install(
                        d.getNode(),
                        new Tooltip(String.format("%s\n$%s\n%s\n%s", e.getName(), e.getPrice(), e.getCategory(),
                                Constants.DATE_FORMAT.format(e.getDate()))));
            }
        }

        if (report.getType() == ReportType.DAILY) {
            scatterChart.setTitle("Today's expenses");
        } else {
            scatterChart.setTitle(report.getType().toString() + " expenses by day");
        }

        return scatterChart;
    }

    /**
     * Creates a {@code PieChart} for the given {@code ReportType}. This chart is split by category.
     *
     * @param reportType the date range to get data for the chart.
     * @return a {@code PieChart} for the given {@code ReportType}.
     */
    private PieChart addPieChart(Report report) {
        ObservableList<PieChart.Data> data = FXCollections.observableArrayList(report.getPieChartData());
        PieChart pieChart = new PieChart(data);
        pieChart.setTitle(report.getType().toString() + " expenses by category");

        // Stretch
        VBox.setVgrow(pieChart, Priority.ALWAYS);
        HBox.setHgrow(pieChart, Priority.ALWAYS);

        for (PieChart.Data slice : pieChart.getData()) {
            // slice.getNode().setStyle("-fx-pie-color: " + Layout.stringToHsbString(slice.getName()) + ";");
            Money absolute = Stats.sum(report.getExpenses()).multiply(new Money(slice.getPieValue() / 100));
            Tooltip.install(slice.getNode(),
                    new Tooltip(String.format("≈$%s\n%2.1f", absolute.toString(), slice.getPieValue()) + "%"));
        }

        return pieChart;
    }
}
